package fast.mvc;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

import org.json.JSONException;

import bloodfang.Bloodfang;
import bloodfang.CycleCombat;
import bloodfang.ProcInfo;
import bloodfang.RaidBuff;
import bloodfang.SetBonus;
import fast.calc.Fast;
import fast.calc.Stats;
import fast.calc.druid.FastDruid;
import fast.calc.monk.FastMonk;
import fast.calc.rogue.FastRogue;
import fast.calc.warlock.FastWarlock;
import fast.data.BaseStats;
import fast.data.CharacterClass;
import fast.data.Stat;
import fast.data.StatBlock;
import fast.data.WeaponType;
import fast.loader.CharacterNotFoundException;
import fast.loader.ItemDB;
import fast.loader.Enchant;
import fast.loader.EnchantDB;
import fast.loader.Equipment;
import fast.loader.Gem;
import fast.loader.GemDB;
import fast.loader.FastLoader;
import fast.loader.Item;
import fast.loader.ItemEquipped;
import fast.loader.Profession;
import fast.loader.Race;
import fast.loader.Reforge;
import fast.loader.Slot;
import fast.loader.Socket;
import fast.loader.SocketBonus;
import fast.loader.Weapon;
import fast.ranking.ItemEquippedEP;

public class Model {

	public Bloodfang bf;
    private Fast fast;
    private ItemDB db;
    private FastLoader loader;
    private Equipment equipment;
    private fast.mvc.Settings settings = new fast.mvc.Settings();
    
	private HashMap<ItemEquipped, ArrayList<Reforge>> item_reforges;
	private ArrayList<ArrayList<ReforgeOption>> list_reforges;
	private HashMap<String, Integer> cap;
	private double bestEP = 0.0;
	private double[] maxEpDelta;
	private HashMap<Item, Double> itemMaxEpDelta;
	private int[] bestOptions;
	private ArrayList<ReforgeOption> results;
	private int called;

    public Model(){
        reset();
    }

    public void reset(){
    	fast = null;
        loader = new FastLoader();
        db = new ItemDB();
		loader.setDatabase(db);
		setEquipment(loader.getEquipment());
		fillEquipment();
    }
    
    private bloodfang.StatBlock convertStatBlock(StatBlock sb){
    	bloodfang.StatBlock bstats = new bloodfang.StatBlock();
    	for(Stat stat : Stat.values()){
			double val = sb.getStat(stat);
			try{
				bloodfang.Stat bname = bloodfang.Stat.valueOf(stat.name());
				if(val != 0){
					bstats.addStat(bname, (int) val);
				}
			}catch(IllegalArgumentException iae){
				//System.out.println(iae.getMessage());
			}
		}
    	return bstats;
    }
    
    public void calcBloodfang(){
    	if(equipment.isEmpty() || equipment.get(Slot.mainHand) == null)
    		return;
    	//BaseStats base = new BaseStats();
    	
    	bloodfang.Race race = bloodfang.Race.valueOf(loader.getBase().getRace().name());
    	
//		base.setSpec(loader.getBase().getSpec());
//		
//		base.setGlyphs(loader.getBase().getGlyphs());
//		
//		base.setBuffs(loader.getBase().getBuffs());
		
		ItemEquipped mhe = equipment.get(Slot.mainHand);
		ItemEquipped ohe = equipment.get(Slot.offHand);
		bloodfang.Weapon mh = new bloodfang.Weapon(mhe.getItem().getDps(),mhe.getItem().getSpeed(),bloodfang.WeaponType.valueOf(mhe.getItem().getWeaponType().name()));
		bloodfang.Weapon oh = new bloodfang.Weapon(ohe.getItem().getDps(),ohe.getItem().getSpeed(),bloodfang.WeaponType.valueOf(ohe.getItem().getWeaponType().name()));
		
		String tal = "221102"; // TODO make it possible to pass other stuff
		
		
		EnumSet<SetBonus> setB = EnumSet.noneOf(SetBonus.class);
		HashSet<ProcInfo> procs = new HashSet<ProcInfo>();
		bloodfang.StatBlock gearStats = new bloodfang.StatBlock();
		ItemEquipped item = null;
		for(Slot name : equipment.keySet()){
			item = equipment.get(name);
			if(item == null)
				continue;
			if(name == Slot.offHand && loader.getBase().getMH().type.isTwoHander())
				continue;
			//base.add(item.getItem());
			gearStats.add(convertStatBlock(item.getItem()));
			//base.add(item.getReforgeAsBlock());
			gearStats.add(convertStatBlock(item.getReforgeAsBlock()));
			
			boolean colorMatch = true;
			for(int j = 0; j < item.getItem().gemSlot.length;j++){
				if(item.getItem().gemSlot[j] == null) continue;
				if(	(name == Slot.wrist || name == Slot.hands)
					&& item.getItem().gemSlot[j] == Socket.prismatic
					&& !loader.getBase().getProfessions().contains(Profession.Blacksmithing) ){
					System.out.println("no blacksmith, so blacksmith sockets will be skipped.");
					continue;
				}
				if(item.getGem(j) != null){
					//base.add(item.getGem(j));
					gearStats.add(convertStatBlock(item.getGem(j)));
					
					int type = item.getGem(j).getType();
					if(item.getItem().gemSlot[j] == Socket.red && (type != Gem.RED && type != Gem.ORANGE && type != Gem.PURPLE)){
						colorMatch = false;
					}
					else if(item.getItem().gemSlot[j] == Socket.yellow && (type != Gem.YELLOW && type != Gem.ORANGE && type != Gem.GREEN)){
						colorMatch = false;
					}
					else if(item.getItem().gemSlot[j] == Socket.blue && (type != Gem.BLUE && type != Gem.PURPLE && type != Gem.GREEN)){
						colorMatch = false;
					}
					else if(item.getItem().gemSlot[j] == Socket.meta && type != Gem.META){
						colorMatch = false;
					}
					if(item.getGem(j).getGearBuff() != null){
						//base.addGearBuff(item.getGem(j).getGearBuff());
						procs.add(new ProcInfo(item.getGem(j).getGearBuff()));
						
					}
				}else if(item.getItem().gemSlot[j] != Socket.prismatic){
					colorMatch = false;
				}
			}
			if(item.getItem().getSocketBonus() != null && colorMatch){
				//base.add(item.getItem().getSocketBonus());
				gearStats.add(convertStatBlock(item.getItem().getSocketBonus()));
			}
			Enchant en = item.getEnchant();
			if(	(name == Slot.finger1 || name == Slot.finger2) 
				&& !loader.getBase().getProfessions().contains(Profession.Enchanting)){
				System.out.println("no enchanter, so ring enchants will be skipped.");
			}
			else if(en != null){
				//base.add(en);
				gearStats.add(convertStatBlock(en));
			}
			// set item
			if(item.getItem().getSet() != null && !item.getItem().getSet().equals("")){
				System.out.println("settt " + item.getItem().getSet());
				//base.addSetItem(item.getItem().getSet());
			}else if(item.getItem().getSet() == null){
				System.out.println("Error: Setitem found which is not in ItemSetDB. ItemID: "+ item.getItem().getId());
			}
			if(item.getItem().getGearBuff() != null){
				//base.addGearBuff(item.getItem().getGearBuff());
				//base.addGearBuffItemLevel(item.getItem().getGearBuff(),item.getItem().getItemLevel());
				procs.add(new ProcInfo(item.getItem().getGearBuff(),item.getItem().getUpgradeLevel()));
			}
		}
		
		if(loader.getBase().getProfessions().contains(Profession.Alchemy)){
			//base.addStat(base.getSpec().getMainStat(), 320);
			gearStats.addStat(bloodfang.Stat.valueOf(loader.getBase().getSpec().getMainStat().name()), 320);
		}
		if(loader.getBase().getProfessions().contains(Profession.Engineering)){
			//base.addGearBuff("synapse_springs");
			procs.add(new ProcInfo("synapse_springs"));
		}
		if(equipment.get(Slot.back) != null && loader.getEnchantDB().getEnchant(4894) != null && equipment.get(Slot.back).getEnchant() == loader.getEnchantDB().getEnchant(4894)){
			//base.addGearBuff("swordguard_embroidery");
			procs.add(new ProcInfo("swordguard_embroidery"));
		}
		if(loader.getBase().getProfessions().contains(Profession.Skinning)){
			//base.addStat(Stat.cri, 480);
			gearStats.addStat(bloodfang.Stat.cri, 480);
		}
		if(loader.getBase().getProfessions().contains(Profession.Herbalism)){
			//base.addGearBuff("lifeblood");
			procs.add(new ProcInfo("lifeblood"));
		}
		if(loader.getBase().getRace() == Race.troll){
			//base.addGearBuff("berserking");
			procs.add(new ProcInfo("berserking"));
		}
		if(loader.getBase().getRace() == Race.orc){
			//base.addGearBuff("blood_fury");
			procs.add(new ProcInfo("blood_fury"));
		}
		if(loader.getBase().getRace() == Race.bloodelf){
			//base.addGearBuff("arcane_torrent");
			procs.add(new ProcInfo("arcane_torrent"));
		}
		if(loader.getBase().getRace() == Race.golbin){
			//base.addGearBuff("time_is_money");
			procs.add(new ProcInfo("time_is_money"));
		}
		if(loader.getBase().getRace() == Race.pandaren){
			//base.addGearBuff("epicurean");
			procs.add(new ProcInfo("epicurean"));
		}
		if(loader.getBase().getRace() == Race.worgen){
			//base.addGearBuff("viciousness");
			procs.add(new ProcInfo("viciousness"));
		}
		if(loader.getBase().getRace() == Race.undead){
			//base.addGearBuff("touch_of_the_grave");
			procs.add(new ProcInfo("touch_of_the_grave"));
		}
		
		// User Settings
//		for(String setting : settings.getActiveSettings()){
//			base.addGearBuff(setting);
//		}

		
    	else if(loader.getBase().getCharacterClass() == CharacterClass.rogue){
    		
    		bf = new Bloodfang();
    		bf.settings = new bloodfang.Settings();
    		
    		bf.stats = new bloodfang.Stats(bf.settings, gearStats, race, mh, oh, tal, EnumSet.allOf(RaidBuff.class), setB, procs);
    		bf.attacks = new bloodfang.Attacks(bf.stats);

    		bf.cycle = new CycleCombat(bf.stats,bf.attacks);
    	}else
    		new Exception("No Class Model found for " + loader.getBase().getCharacterClass());
    }
    
    public void calcFast(){
    	if(equipment.isEmpty() || equipment.get(Slot.mainHand) == null)
    		return;
    	BaseStats base = new BaseStats();
    	
    	base.setCharacterClass(loader.getBase().getCharacterClass());
		base.setRace(loader.getBase().getRace());
		base.setSpec(loader.getBase().getSpec());
		base.setTalents(loader.getBase().getTalents());
		base.setGlyphs(loader.getBase().getGlyphs());
		base.setBuffs(loader.getBase().getBuffs());
		
		ItemEquipped mhe = equipment.get(Slot.mainHand);
		System.out.println(mhe.toString());
		String enchant = "";
		if(mhe.getEnchant() != null)
			enchant = mhe.getEnchant().getName();
		Weapon mh = new Weapon(mhe.getItem().getSpeed(),mhe.getItem().getDps(),mhe.getItem().getWeaponType(),enchant);
		base.setMH(mh);
		
		ItemEquipped ohe = equipment.get(Slot.offHand);
		if(ohe != null){
			enchant = "";
			if(ohe.getEnchant() != null)
				enchant = ohe.getEnchant().getName();
			Weapon oh = new Weapon(ohe.getItem().getSpeed(),ohe.getItem().getDps(),ohe.getItem().getWeaponType(),enchant);
			base.setOH(oh);
		}
		
		ItemEquipped item = null;
		for(Slot name: equipment.keySet()){
			item = equipment.get(name);
			if(item == null)
				continue;
			if(name == Slot.offHand && base.getMH().type.isTwoHander())
				continue;
			base.add(item.getItem());
			base.add(item.getReforgeAsBlock());
			
			boolean colorMatch = true;
			for(int j = 0; j < item.getItem().gemSlot.length;j++){
				if(item.getItem().gemSlot[j] == null) continue;
				if(	(name == Slot.wrist || name == Slot.hands)
					&& item.getItem().gemSlot[j] == Socket.prismatic
					&& !loader.getBase().getProfessions().contains(Profession.Blacksmithing) ){
					System.out.println("no blacksmith, so blacksmith sockets will be skipped.");
					continue;
				}
				if(item.getGem(j) != null){
					base.add(item.getGem(j));
					
					int type = item.getGem(j).getType();
					if(item.getItem().gemSlot[j] == Socket.red && (type != Gem.RED && type != Gem.ORANGE && type != Gem.PURPLE)){
						colorMatch = false;
					}
					else if(item.getItem().gemSlot[j] == Socket.yellow && (type != Gem.YELLOW && type != Gem.ORANGE && type != Gem.GREEN)){
						colorMatch = false;
					}
					else if(item.getItem().gemSlot[j] == Socket.blue && (type != Gem.BLUE && type != Gem.PURPLE && type != Gem.GREEN)){
						colorMatch = false;
					}
					else if(item.getItem().gemSlot[j] == Socket.meta && type != Gem.META){
						colorMatch = false;
					}
					if(item.getGem(j).getGearBuff() != null){
						base.addGearBuff(item.getGem(j).getGearBuff());
					}
				}else if(item.getItem().gemSlot[j] != Socket.prismatic){
					colorMatch = false;
				}
			}
			if(item.getItem().getSocketBonus() != null && colorMatch){
				base.add(item.getItem().getSocketBonus());
			}
			Enchant en = item.getEnchant();
			if(	(name == Slot.finger1 || name == Slot.finger2) 
				&& !loader.getBase().getProfessions().contains(Profession.Enchanting)){
				System.out.println("no enchanter, so ring enchants will be skipped.");
			}
			else if(en != null){
				base.add(en);
			}
			// set item
			if(item.getItem().getSet() != null && !item.getItem().getSet().equals("")){
				//System.out.println("tt " + item.getSet());
				base.addSetItem(item.getItem().getSet());
			}else if(item.getItem().getSet() == null){
				System.out.println("Error: Setitem found which is not in ItemSetDB. ItemID: "+ item.getItem().getId());
			}
			if(item.getItem().getGearBuff() != null){
				base.addGearBuff(item.getItem().getGearBuff());
				base.addGearBuffItemLevel(item.getItem().getGearBuff(),item.getItem().getItemLevel());
			}
		}
		// Race
		Race race = base.getRace();
		if(race != null){
			base.add(race.getStats());
		}
		
		if(loader.getBase().getProfessions().contains(Profession.Alchemy)){
			base.addStat(base.getSpec().getMainStat(), 320);
		}
		if(loader.getBase().getProfessions().contains(Profession.Engineering)){
			base.addGearBuff("synapse_springs");
		}
		if(equipment.get(Slot.back) != null && loader.getEnchantDB().getEnchant(4894) != null && equipment.get(Slot.back).getEnchant() == loader.getEnchantDB().getEnchant(4894)){
			base.addGearBuff("swordguard_embroidery");
		}
		if(loader.getBase().getProfessions().contains(Profession.Skinning)){
			base.addStat(Stat.cri, 480);
		}
		if(loader.getBase().getProfessions().contains(Profession.Herbalism)){
			base.addGearBuff("lifeblood");
		}
		if(loader.getBase().getRace() == Race.troll){
			base.addGearBuff("berserking");
		}
		if(loader.getBase().getRace() == Race.orc){
			base.addGearBuff("blood_fury");
		}
		if(loader.getBase().getRace() == Race.bloodelf){
			base.addGearBuff("arcane_torrent");
		}
		if(loader.getBase().getRace() == Race.golbin){
			base.addGearBuff("time_is_money");
		}
		if(loader.getBase().getRace() == Race.pandaren){
			base.addGearBuff("epicurean");
		}
		if(loader.getBase().getRace() == Race.worgen){
			base.addGearBuff("viciousness");
		}
		if(loader.getBase().getRace() == Race.undead){
			base.addGearBuff("touch_of_the_grave");
		}
		
		// User Settings
		for(String setting : settings.getActiveSettings()){
			base.addGearBuff(setting);
		}

    	if(base.getCharacterClass() == CharacterClass.monk)
    		fast = new FastMonk(base);
    	else if(base.getCharacterClass() == CharacterClass.druid)
    		fast = new FastDruid(base);
    	else if(base.getCharacterClass() == CharacterClass.rogue)
    		fast = new FastRogue(base);
    	else if(base.getCharacterClass() == CharacterClass.warlock)
    		fast = new FastWarlock(base);
    	else
    		new Exception("No Class Model found for " + base.getCharacterClass());
    }
    
    public void calcFast(StatBlock stats){
    	if(equipment.isEmpty() || equipment.get(Slot.mainHand) == null)
    		return;
    	BaseStats base = new BaseStats();
    	
    	base.setCharacterClass(loader.getBase().getCharacterClass());
		base.setRace(loader.getBase().getRace());
		base.setSpec(loader.getBase().getSpec());
		base.setTalents(loader.getBase().getTalents());
		base.setGlyphs(loader.getBase().getGlyphs());
		base.setBuffs(loader.getBase().getBuffs());
		
		ItemEquipped mhe = equipment.get(Slot.mainHand);
		String enchant = "";
		if(mhe.getEnchant() != null)
			enchant = mhe.getEnchant().getName();
		Weapon mh = new Weapon(mhe.getItem().getSpeed(),mhe.getItem().getDps(),mhe.getItem().getWeaponType(),enchant);
		base.setMH(mh);
		
		ItemEquipped ohe = equipment.get(Slot.offHand);
		if(ohe != null){
			enchant = "";
			if(ohe.getEnchant() != null)
				enchant = ohe.getEnchant().getName();
			Weapon oh = new Weapon(ohe.getItem().getSpeed(),ohe.getItem().getDps(),ohe.getItem().getWeaponType(),enchant);
			base.setOH(oh);
		}
		
		base.add(stats);
		ItemEquipped item = null;
		for(Slot name: equipment.keySet()){
			item = equipment.get(name);
			if(item == null)
				continue;
			if(name == Slot.offHand && base.getMH().type.isTwoHander())
				continue;
			
			for(int j = 0; j < item.getItem().gemSlot.length;j++){
				if(item.getItem().gemSlot[j] == null) continue;
				if(	(name == Slot.wrist || name == Slot.hands)
					&& item.getItem().gemSlot[j] == Socket.prismatic
					&& !loader.getBase().getProfessions().contains(Profession.Blacksmithing) ){
					System.out.println("no blacksmith, so blacksmith sockets will be skipped.");
					continue;
				}
				if(item.getGem(j) != null){
					if(item.getGem(j).getGearBuff() != null){
						base.addGearBuff(item.getGem(j).getGearBuff());
					}
				}
			}
			// set item
			if(item.getItem().getSet() != null && !item.getItem().getSet().equals("")){
				//System.out.println("tt " + item.getSet());
				base.addSetItem(item.getItem().getSet());
			}else if(item.getItem().getSet() == null){
				System.out.println("Error: Setitem found which is not in ItemSetDB. ItemID: "+ item.getItem().getId());
			}
			if(item.getItem().getGearBuff() != null){
				base.addGearBuff(item.getItem().getGearBuff());
				base.addGearBuffItemLevel(item.getItem().getGearBuff(),item.getItem().getItemLevel());
			}
		}
		// Race
		Race race = base.getRace();
		if(race != null){
			base.add(race.getStats());
		}
		
		if(loader.getBase().getProfessions().contains(Profession.Alchemy)){
			base.addStat(base.getSpec().getMainStat(), 320);
		}
		if(loader.getBase().getProfessions().contains(Profession.Engineering)){
			base.addGearBuff("synapse_springs");
		}
		if(equipment.get(Slot.back) != null && loader.getEnchantDB().getEnchant(4894) != null && equipment.get(Slot.back).getEnchant() == loader.getEnchantDB().getEnchant(4894)){
			base.addGearBuff("swordguard_embroidery");
		}
		if(loader.getBase().getProfessions().contains(Profession.Skinning)){
			base.addStat(Stat.cri, 480);
		}
		if(loader.getBase().getProfessions().contains(Profession.Herbalism)){
			base.addGearBuff("lifeblood");
		}
		if(loader.getBase().getRace() == Race.troll){
			base.addGearBuff("berserking");
		}
		if(loader.getBase().getRace() == Race.orc){
			base.addGearBuff("blood_fury");
		}
		if(loader.getBase().getRace() == Race.bloodelf){
			base.addGearBuff("arcane_torrent");
		}
		if(loader.getBase().getRace() == Race.golbin){
			base.addGearBuff("time_is_money");
		}
		if(loader.getBase().getRace() == Race.pandaren){
			base.addGearBuff("epicurean");
		}
		if(loader.getBase().getRace() == Race.worgen){
			base.addGearBuff("viciousness");
		}
		if(loader.getBase().getRace() == Race.undead){
			base.addGearBuff("touch_of_the_grave");
		}
		
		// User Settings
		for(String setting : settings.getActiveSettings()){
			base.addGearBuff(setting);
		}

    	if(base.getCharacterClass() == CharacterClass.monk)
    		fast = new FastMonk(base);
    	else if(base.getCharacterClass() == CharacterClass.druid)
    		fast = new FastDruid(base);
    	else if(base.getCharacterClass() == CharacterClass.rogue)
    		fast = new FastRogue(base);
    	else if(base.getCharacterClass() == CharacterClass.warlock)
    		fast = new FastWarlock(base);
    	else
    		new Exception("No Class Model found for " + base.getCharacterClass());
    }
    
    public double getDPS(){
    	if(fast == null)
    		return 0.0;
    	return fast.getDps();
    }

	public void loadCharacter(String region, String server, String player) throws CharacterNotFoundException {
		try {
			System.out.println("Importing " + player + " from " + server + "-"+ region);
			loader.load2(player, server, region);
			setEquipment(loader.getEquipment());
		} 
		catch (JSONException e) {
			e.printStackTrace();
		}
	}
	
	public ItemEquippedEP calcItemEP2(Item item,Slot slot,ItemEquipped curItem){
		if(fast == null){
			ItemEquipped ie = new ItemEquipped();
			ie.setItem(item);
			return new ItemEquippedEP(ie, item.getItemLevel());
		}
		
		// NEW curItem will be replaced so the actual stats
		// of the items with gems and reforge should be truncated
		// from overall stats
		StatBlock curItemStats = new StatBlock();
		curItemStats.add(curItem.getItem());
		//curItemStats.add(curItem.getReforgeAsBlock());
		for(int i = 0; i < curItem.gem.length; i++){
			if(curItem.getGem(i) != null){
				curItemStats.add(curItem.getGem(i));
			}
		}
		if(curItem.gemsMatch() && curItem.getItem().getSocketBonus() != null){
			curItemStats.add(curItem.getItem().getSocketBonus());
		}
		
		double ep = 0;

		if(Slot.mainHand == slot){
			ep += fast.getEP().get("mh_dps")*item.getDps();
			if(item.getSpeed() >= 1.4)
				ep += fast.getEP().get("mh_speed_"+item.getSpeed());
			ep += fast.getEP().get("mh_type_"+item.getWeaponType().name());
		}else if(Slot.offHand == slot){
			ep += fast.getEP().get("oh_dps")*item.getDps();
			if(item.getSpeed() >= 1.4)
				ep += fast.getEP().get("oh_speed_"+item.getSpeed());
			ep += fast.getEP().get("oh_type_"+item.getWeaponType().name());
		}
		
		// NEW STUFF FIRST
		
		// NEW gems matters if socketbonus is hit or exp
		// or one socket is blue
		boolean sb_matters = false;
		if(item.getSocketBonus() != null){
			SocketBonus sb = item.getSocketBonus();
			sb_matters = hasBlockStat(sb, Stat.hit) || hasBlockStat(sb, Stat.exp);
			//System.out.println("SocketBonus: " + matters);
		}
		boolean gem_matters = false;
		Socket[] gemSlot = item.getGemSlot();
		for(int i = 0;i< gemSlot.length;i++){
			if(gemSlot[i] == Socket.blue){
				gem_matters = true;
				break;
			}
		}
		// if sb_matters or gem_matters we need to handle all this too
		// else just use the best option available
		
		// NEW gems
		ArrayList<Gem[]> gemmingOptions = getGemmingOptions(item);

		// NEW reforge
		ArrayList<Reforge> reforgeOptions = getReforgeOptions(item);
		
		// find the best conjunction of gems and reforges
		if(item.getItemLevel() == 522)
			System.out.println(item.getName() + " " + item.getItemLevel());
		double bestEP = 0.0;
		ItemEquipped ie = new ItemEquipped();
		ie.setItem(item);
		ItemEquipped bestOption = ie;
		for(Gem[] gems : gemmingOptions){
			for(Reforge reforge : reforgeOptions){
				ItemEquipped test = new ItemEquipped();
				test.setItem(item);
				test.setReforge(reforge);
				/*if(reforge != null)
					System.out.println("..with reforge " + reforge.getFrom() + " -> " + reforge.getTo());
				else
					System.out.println("..without reforge");*/
				StatBlock itemStats = new StatBlock();
				itemStats.add(item);
				itemStats.add(test.getReforgeAsBlock());
				test.setGem(0, gems[0]);
				test.setGem(1, gems[1]);
				test.setGem(2, gems[2]);
				for(int i = 0; i < test.gem.length; i++){
					if(test.getGem(i) != null){
						itemStats.add(test.getGem(i));
					}
				}
				if(test.gemsMatch() && item.getSocketBonus() != null){
					itemStats.add(item.getSocketBonus());
				}
				/*for(Stat stat : Stat.possibleStats){
					double value = itemStats.getStat(stat);
					if(value != 0)
						System.out.println(stat + " " + value);
				}*/
				double option_ep = calcStatBlockEP(itemStats);
				System.out.println(bestOption);
				System.out.println(option_ep);
				if(option_ep > bestEP){
					bestEP = option_ep;
					bestOption = test;
				}
			}
		}
		if(item.getItemLevel() == 522){
			System.out.println("bestOption:");
			for(Gem gems : bestOption.gem){
				if(gems != null)
					System.out.println(gems + " " + gems.getType());
			}
			if(bestOption.getReforge() != null)
				System.out.println(bestOption.getReforge().getFrom().getName() + " -> " + bestOption.getReforge().getTo().getName());
			System.out.println("item_ep: " + bestEP);
		}
		ep += bestEP;
		
		// gearbuffs...
		if(item.getGearBuff() != null){
			try{
				//ep += fast.getEP().get(item.getGearBuff()+"_"+item.getItemLevel());
				ep += bf.ep_procs.get(item.getGearBuff())[item.getUpgradeLevel()];
			}catch(Exception e){
				//View.showError("missing: " + item.getName() + " " + item.getGearBuff()+"_"+item.getItemLevel());
				System.err.println("error: " + item.getGearBuff());
				//ep += fast.getEP().get(item.getGearBuff());
			}
		}
		return new ItemEquippedEP(bestOption, ep);
	}
	
	public double calcItemEP(Item item,Slot slot){
		if(fast == null)
			return item.getItemLevel();
		double ep = calcStatBlockEP(item);

		if(Slot.mainHand == slot){
			ep += fast.getEP().get("mh_dps")*item.getDps();
			if(item.getSpeed() >= 1.4)
				ep += fast.getEP().get("mh_speed_"+item.getSpeed());
			ep += fast.getEP().get("mh_type_"+item.getWeaponType().name());
		}else if(Slot.offHand == slot){
			ep += fast.getEP().get("oh_dps")*item.getDps();
			if(item.getSpeed() >= 1.4)
				ep += fast.getEP().get("oh_speed_"+item.getSpeed());
			ep += fast.getEP().get("oh_type_"+item.getWeaponType().name());
		}
		
		// gems
		Gem bestRed = getBestGem(Socket.red);
		Gem bestYellow = getBestGem(Socket.yellow);
		Gem bestBlue = getBestGem(Socket.blue);
		Gem bestHydraulic = getBestGem(Socket.hydraulic);
		
		double bestRedEP = calcGemEP(bestRed);
		double bestYellowEP = calcGemEP(bestYellow);
		double bestBlueEP = calcGemEP(bestBlue);
		double bestHydraulicEP = 0.0;
		if(bestHydraulic != null)
			bestHydraulicEP = calcGemEP(bestHydraulic);
		
		//Gem bestGem = bestRed;
		double bestGemEP = bestRedEP;
		if(bestYellowEP > bestGemEP){
			//bestGem = bestYellow; 
			bestGemEP = bestYellowEP;
		}
		if(bestBlueEP > bestRedEP){
			//bestGem = bestBlue; 
			bestGemEP = bestBlueEP;
		}
		double gemMatchEP = 0.;
		Socket[] gemSlot = item.getGemSlot();
		for(int i = 0;i< gemSlot.length;i++){
			if(gemSlot[i] == Socket.prismatic)
				gemMatchEP += bestGemEP;
			if(gemSlot[i] == Socket.red){
				gemMatchEP += bestRedEP;
			}else if(gemSlot[i] == Socket.yellow){
				gemMatchEP += bestYellowEP;
			}else if(gemSlot[i] == Socket.blue){
				gemMatchEP += bestBlueEP;
			}else if(gemSlot[i] == Socket.hydraulic){
				gemMatchEP += bestHydraulicEP;
			}
		}
		
		//System.out.println("gem Match EP: " + gemMatchEP);
		
		double gemOnlyBestEP = 0.;
		//boolean gemOnlyBestMatch = true;
		for(int i = 0;i< gemSlot.length;i++){
			if(gemSlot[i] == null) continue;
			gemOnlyBestEP += bestGemEP;
		}
		
		//System.out.println(gemOnlyBestEP);
		
		double gemSocketEP = 0.;
		if(item.getSocketBonus() != null){
			gemSocketEP = calcStatBlockEP(item.getSocketBonus());
		}
		//System.out.println("gem socket ep:" + item.getName() + " " + gemSocketEP);
		gemMatchEP += gemSocketEP;
		
		//if(gemOnlyBestMatch)
		//	gemOnlyBestEP += gemSocketEP;
		//System.out.println("gemMatchEP" + item.getName() + " " + gemMatchEP);
		//System.out.println("gemOnlyBestEP" + item.getName() + " " + gemOnlyBestEP);
		ep += Math.max(gemMatchEP, gemOnlyBestEP);
		
		Stat min = null;
		Stat max = null;
		
	    for(Stat stat : new Stat[]{Stat.exp,Stat.hit,Stat.cri,Stat.hst,Stat.mst}) {
	    	if(hasBlockStat(item,stat) && (min == null || fast.getEP().get(min.getEpName()) > fast.getEP().get(stat.getEpName()))) {
		        min = stat;
		    }
		    if(!hasBlockStat(item,stat) && (max == null || fast.getEP().get(max.getEpName()) < fast.getEP().get(stat.getEpName()))) {
		    	max = stat;
		    }
	    }
		
		if(min != null && max != null){
			int value = (int) Math.floor(item.getStat(min) * 0.4);
			double lowEP = fast.getEP().get(min.getEpName()) * value;
			double highEP = fast.getEP().get(max.getEpName()) * value;
			ep += Math.max(highEP - lowEP, 0);
		}
		
		// gearbuffs...
		if(item.getGearBuff() != null){
			try{
				ep += fast.getEP().get(item.getGearBuff()+"_"+item.getItemLevel());
			}catch(Exception e){
				//View.showError("missing: " + item.getName() + " " + item.getGearBuff()+"_"+item.getItemLevel());
				//System.out.println("error: " + item.getGearBuff());
				ep += fast.getEP().get(item.getGearBuff());
			}
		}
		return ep;
	}
	
	public boolean hasBlockStat(StatBlock statBlock, Stat stat){
		if(statBlock.getStat(stat) > 0)
			return true;
		return false;
	}
	
	public ArrayList<Gem[]> getGemmingOptions(Item item){
		ArrayList<Gem[]> options = new ArrayList<Gem[]>();
		
		// gems
		Gem bestRed = getBestGem(Socket.red);
		Gem bestYellow = getBestGem(Socket.yellow);
		Gem bestBlue = getBestGem(Socket.blue);
		Gem bestHydraulic = getBestGem(Socket.hydraulic);
		Gem bestMeta = getBestGem(Socket.meta);
		
		double bestRedEP = calcGemEP(bestRed);
		double bestYellowEP = calcGemEP(bestYellow);
		double bestBlueEP = calcGemEP(bestBlue);
		double bestHydraulicEP = 0.0;
		if(bestHydraulic != null)
			bestHydraulicEP = calcGemEP(bestHydraulic);
		double bestMetaEP = calcGemEP(bestMeta);
		
		Gem bestGem = bestRed;
		double bestGemEP = bestRedEP;
		if(bestYellowEP > bestGemEP){
			bestGem = bestYellow; 
			bestGemEP = bestYellowEP;
		}
		if(bestBlueEP > bestRedEP){
			bestGem = bestBlue; 
			bestGemEP = bestBlueEP;
		}
		Gem[] bestMatch = new Gem[3];
		double gemMatchEP = 0.;
		Socket[] gemSlot = item.getGemSlot();
		for(int i = 0;i < gemSlot.length;i++){
			if(gemSlot[i] == Socket.prismatic){
				gemMatchEP += bestGemEP;
				bestMatch[i] = bestGem;
			}else if(gemSlot[i] == Socket.red){
				gemMatchEP += bestRedEP;
				bestMatch[i] = bestRed;
			}else if(gemSlot[i] == Socket.yellow){
				gemMatchEP += bestYellowEP;
				bestMatch[i] = bestYellow;
			}else if(gemSlot[i] == Socket.blue){
				gemMatchEP += bestBlueEP;
				bestMatch[i] = bestBlue;
			}else if(gemSlot[i] == Socket.hydraulic){
				gemMatchEP += bestHydraulicEP;
				bestMatch[i] = bestHydraulic;
			}else if(gemSlot[i] == Socket.meta){
				gemMatchEP += bestMetaEP;
				bestMatch[i] = bestMeta;
			}
		}
		options.add(bestMatch);
		
		// bestOnly
		Gem[] bestOnly = new Gem[3];
		for(int i = 0;i < gemSlot.length;i++){
			if(gemSlot[i] == null) continue;
			if(gemSlot[i] == Socket.meta){
				bestOnly[i] = bestMeta;
				continue;
			}
			bestOnly[i] = bestGem;
		}
		options.add(bestOnly);
	
		return options;
	}
	
	public ArrayList<Reforge> getReforgeOptions(Item item){
		ArrayList<Reforge> reforgeOptions = new ArrayList<Reforge>();
		for ( Stat from : Stat.possibleReforgeStats ){
			for( Stat to: Stat.possibleReforgeStats ){
				if(from == to) continue;
				if(hasBlockStat(item, from) && !hasBlockStat(item, to)){
					reforgeOptions.add(new Reforge(from,to));
				}
			}
		}
		// adding no reforge, its also possible...
		reforgeOptions.add(null);
		System.out.println(reforgeOptions);
		return reforgeOptions;
	}
	
	public ArrayList<ReforgeOption> getReforgeOptions2(Item item){
		Map<String, Double> ep = fast.getEP();
		ArrayList<ReforgeOption> reforgeOptions = new ArrayList<ReforgeOption>();
		for ( Stat from : Stat.possibleReforgeStats ){
			for( Stat to: Stat.possibleReforgeStats ){
				if(from == to) continue;
				if(Arrays.asList(Stat.spi,Stat.parry,Stat.dodge).contains(to)) continue;
				if(hasBlockStat(item, from) && !hasBlockStat(item, to)){
					reforgeOptions.add(new ReforgeOption(item,new Reforge(from,to)));
				}
			}
		}
		// adding no reforge, its also possible...
		reforgeOptions.add(new ReforgeOption(item,null));
		return reforgeOptions;
	}
	
	public Reforge getBestReforge(Item item){
		ArrayList<Reforge> items = new ArrayList<Reforge>();
		for ( Stat from : Stat.possibleReforgeStats ){
			for( Stat to: Stat.possibleReforgeStats ){
				if(from == to) continue;
				if(hasBlockStat(item, from) && !hasBlockStat(item, to)){
					items.add(new Reforge(from,to));
				}
			}
		}
		items.add(null);
		double best = 0;
		Reforge bestReforge = null;
		for ( Reforge elem : items ){
			double reforgeEP = calcReforgeEP(item, elem);
			if(bestReforge == null || reforgeEP > best){
				bestReforge = elem;
				best = reforgeEP;
			}
		}
		return bestReforge;
	}
	
	public Enchant getBestEnchant(Slot s){
		String slotName = s.name();
        if(slotName.endsWith("1") || slotName.endsWith("2")){
        	slotName = slotName.substring(0, slotName.length()-1);
        }else if(slotName.equals("mainHand") || slotName.equals("offHand")){
        	slotName = "weapon";
        }
		
		EnchantDB enchantDB = loader.getEnchantDB();
		
		boolean isPlayerEnchanter = loader.getBase().getProfessions().contains(Profession.Enchanting);
		boolean isPlayerLeatherworker = loader.getBase().getProfessions().contains(Profession.Leatherworking);
		boolean isPlayerInscripter = loader.getBase().getProfessions().contains(Profession.Inscription);
		boolean isPlayerTailor = loader.getBase().getProfessions().contains(Profession.Tailoring);
		double best = 0;
		Enchant bestEnchant = null;
		for ( Enchant elem : enchantDB.getDB().values() ){
			// enchanter only enchants
			if(elem.isEnchanterOnly() && !isPlayerEnchanter){
				continue;
			}
			if(elem.isLeatherworkerOnly() && !isPlayerLeatherworker){
				continue;
			}
			if(elem.isInscripterOnly() && !isPlayerInscripter){
				continue;
			}
			if(elem.isTailorOnly() && !isPlayerTailor){
				continue;
			}
			if(elem.getStat(Stat.exp) > 0 && settings.isIgnoreExp()){
				continue;
			}
			if(elem.getSlotName().equals(slotName)){
				double enchantEP = calcEnchantEP(elem, s);
				if(bestEnchant == null || enchantEP > best){
					bestEnchant = elem;
					best = enchantEP;
				}
			}	

		}
		return bestEnchant;
	}
	
	public Gem getBestGem(Socket gemSlot){
		GemDB gemDB = loader.getGemDB();
		boolean isPlayerJC = false; loader.getBase().getProfessions().contains(Profession.Jewelcrafting);
		boolean isPlayerEng = false; loader.getBase().getProfessions().contains(Profession.Engineering);
		double best = 0;
		Gem bestGem = null;
		for ( Gem elem : gemDB.getDB().values() ){
			// 755 jewelcrafting
			if(elem.getReqSkill() == 755 && !isPlayerJC){
				continue;
			}
			else if(elem.getReqSkill() == 202 && !isPlayerEng){
				continue;
			}
			if(elem.getStat(Stat.exp) > 0 && settings.isIgnoreExp()){
				continue;
			}
			boolean valid_gem = false;
			if(gemSlot == Socket.meta){
				if(elem.getType() == Gem.META)
					valid_gem = true;
			}
			else if(gemSlot == Socket.red){
				if(elem.getType() == Gem.RED || elem.getType() == Gem.ORANGE || elem.getType() == Gem.PURPLE)
					valid_gem = true;
			}
			else if(gemSlot == Socket.yellow){
				if(elem.getType() == Gem.YELLOW || elem.getType() == Gem.ORANGE || elem.getType() == Gem.GREEN)
					valid_gem = true;
			}
			else if(gemSlot == Socket.blue){
				if(elem.getType() == Gem.BLUE || elem.getType() == Gem.GREEN || elem.getType() == Gem.PURPLE)
					valid_gem = true;
			}
			else if(gemSlot == Socket.prismatic){
				if(elem.getType() != Gem.COGWHEEL && elem.getType() != Gem.HYDRAULIC && elem.getType() != Gem.META)
					valid_gem = true;
			}
			/*else if(gemSlot == Socket.cogwheel)){
				if(elem.getType() == Gem.COGWHEEL)
					valid_gem = true;
			}*/
			else if(gemSlot == Socket.hydraulic){
				if(elem.getType() == Gem.HYDRAULIC)
					valid_gem = true;
			}
			double gemEP = calcGemEP(elem);
			if(valid_gem && (bestGem == null || gemEP > best)){
				bestGem = elem;
				best = gemEP;
			}
		}
		return bestGem;
	}
	
	private double calcStatBlockEP(StatBlock stats){
		return calcStatBlockEP(stats,StatBlock.ZERO_BLOCK);
	}
	
	private double calcStatBlockEP(StatBlock stats,StatBlock truncate){
		Map<String,Double> statsEP = fast.getEP();
		double ep = 0.0;
		for(Stat stat : Stat.possibleStats){
			double value = stats.getStat(stat);
			if(statsEP.get(stat.getEpName()) != null && value != 0){
				if(stat == Stat.hit){
					double cap = 2550; // TODO get this from other source		
					double overall = this.getStats().getStat(stat) - truncate.getStat(stat);
					if(truncate.getStat(stat) == 0)
						cap += 0.6 * value;
					double under_cap = Math.max(cap - overall,0);
//					if(truncate != StatBlock.ZERO_BLOCK){
//						System.out.println("truncate: " + truncate);
//						System.out.println("overall: " + overall);
//						System.out.println("value: " + value);
//						System.out.println("new cap: " + (cap + 0.6 * value));
//						System.out.println("hit_under_cap: " + under_cap);
//						System.out.println("---");
//					}
					ep += under_cap * statsEP.get("style_hit") + (value - under_cap) * statsEP.get("white_hit");
				}
				else if(stat == Stat.exp){
					double cap = 2210; // TODO get this from other source			
					double overall = this.getStats().getStat(stat) - truncate.getStat(stat);
					double under_cap = Math.max(cap - overall,0);
					System.out.println("truncate: " + truncate);
					System.out.println("overall: " + overall);
					System.out.println("value: " + value);
					System.out.println("new cap: " + (cap + 0.6 * value));
					System.out.println("exp_under_cap: " + under_cap);
					System.out.println("---");
					ep += under_cap * statsEP.get(stat.getEpName());
				}
				else{
					double weight = statsEP.get(stat.getEpName());
					ep += weight * value;
				}
			}
		}
		return ep;
	}
	
	// for reforge purpose
	private double calcReforgeBlockEP(StatBlock stats){
		called++;
		Map<String,Double> statsEP = fast.getEP();
		double ep = 0.0;
		for(Stat stat : Stat.possibleStats){
			double value = stats.getStat(stat);
			if(statsEP.get(stat.getEpName()) != null && value != 0){
				if(stat == Stat.hit){
					double style_cap = cap.get("style_hit");	
					//double white_cap = cap.get("white_hit");
					
					if(value > style_cap){
						ep += style_cap * statsEP.get("style_hit");
						ep += (value - style_cap) * statsEP.get("white_hit");
					}else{
						ep += value * statsEP.get("style_hit");
					}
					
				}
				else if(stat == Stat.exp){
					double mh_exp_cap = cap.get("mh_exp");
					double oh_exp_cap = cap.get("oh_exp");
					if(value > mh_exp_cap){
						ep += mh_exp_cap * cap.get("mh_exp");
					}else{
						ep += value * cap.get("mh_exp");
					}
					if(value > oh_exp_cap){
						ep += oh_exp_cap * cap.get("oh_exp");
					}else{
						ep += value * cap.get("oh_exp");
					}
				}
				else{
					double weight = statsEP.get(stat.getEpName());
					ep += weight * value;
				}
			}
		}
		return ep;
	}
	
	public double calcGemEP(Item item, Gem gem,int gemSlot) {
		if(fast == null){
			double ep = gem.getQuality()*10000;
			for(Stat stat : Stat.possibleStats){
				if(!Arrays.asList(Stat.possibleReforgeStats).contains(stat))
					ep += gem.getStat(stat)*2;
				else
					ep += gem.getStat(stat);
			}
			return ep;
		}
		double ep = calcStatBlockEP(gem);
		
		// bring in some conditions like socketbonus
		if(equipment.doesSocketMatch(item.gemSlot[gemSlot],gem)){
			double gemSocketEP = 0.;
			if(item.getSocketBonus() != null){
				gemSocketEP = calcStatBlockEP(item.getSocketBonus());
			}
			ep += gemSocketEP;
		}
		// gearbuffs...
		if(gem.getGearBuff() != null){
			try {
				ep += fast.getEP().get(gem.getGearBuff());
			}catch(Exception e){
				View.showError("missing: " + gem.getName() + " " + gem.getGearBuff());
			}
		}
		
		return ep;
	}
	
	public double calcGemEP(Gem gem) {
		if(fast == null){
			double ep = gem.getQuality()*10000;
			for(Stat stat : Stat.possibleStats){
				if(!Arrays.asList(Stat.possibleReforgeStats).contains(stat))
					ep += gem.getStat(stat)*2;
				else
					ep += gem.getStat(stat);
			}
			return ep;
		}
		double ep = calcStatBlockEP(gem);
		// gearbuffs...
		if(gem.getGearBuff() != null){
			try {
				ep += fast.getEP().get(gem.getGearBuff());
			}catch(Exception e){
				View.showError("missing: " + gem.getName() + " " + gem.getGearBuff());
			}
		}
		return ep;
	}
	
	public double calcEnchantEP(Enchant e,Slot slot) {
		if(fast == null || e == null)
			return 0.0;
		Map<String,Double> statsEP = fast.getEP();
		double ep = calcStatBlockEP(e);
		
		if(Slot.mainHand == slot && statsEP.get("mh_"+e.getName()) != null){
			ep += statsEP.get("mh_"+e.getName());
		}else if(Slot.offHand == slot && statsEP.get("oh_"+e.getName()) != null){
			ep += statsEP.get("oh_"+e.getName());
		}else if(Slot.back == slot && statsEP.get(e.getName()) != null){
			ep += statsEP.get(e.getName());
		}
		
		return ep;
	}
	
	public double calcReforgeEP(Item i,Reforge r){
		if(fast == null || i == null || r == null)
			return 0.0;
		Map<String,Double> statsEP = fast.getEP();
		int value = 0;
		double ep = 0;
		value = (int) Math.floor(i.getStat(r.getFrom()) * 0.4);
		if(statsEP.get(r.getFrom().getEpName()) != null)
			value = (int) Math.floor(i.getStat(r.getFrom()) * 0.4);
			ep -= value * statsEP.get(r.getFrom().getEpName());
		if(statsEP.get(r.getTo().getEpName()) != null)
			ep += value * statsEP.get(r.getTo().getEpName());
		return ep;
	}
	
	public int getRaceWeaponSpecilizationReduction(Slot slot){
		if(!Arrays.asList(Slot.mainHand,Slot.offHand).contains(slot))
			return 0;
		ItemEquipped mh2 = equipment.get(slot);
		Item mh = mh2.getItem();
		WeaponType wt = mh.getWeaponType();
		Race race = loader.getBase().getRace();
		if(wt == WeaponType.dagger && (race == Race.gnome))
			return 340;
		if(wt == WeaponType.sword && (race == Race.human || race == Race.gnome))
			return 340;
		if(wt == WeaponType.mace && (race == Race.human || race == Race.dwarf))
			return 340;
		if(wt == WeaponType.axe && (race == Race.orc))
			return 340;
		if(wt == WeaponType.fist && (race == Race.orc))
			return 340;
		return 0;
	}
	
	public void fillEquipment() {
		for(Slot slot : Slot.values()){
			this.equipment.put(slot, new ItemEquipped());
		}
	}

	public void setEquipment(Equipment equipment) {
		this.equipment = equipment;
	}

	public Equipment getEquipment() {
		return equipment;
	}
	
	public FastLoader getLoader() {
		return loader;
	}

	public ItemDB getDatabase() {
		return db;
	}
	
	public Stats getStats(){
		return fast.getStats();
	}

	public Map<String,Double> getEP() {
		if(fast == null)
			return new HashMap<String,Double>();
		return fast.getEP();
	}

	public void setSettings(fast.mvc.Settings settings) {
		this.settings = settings;
	}

	public fast.mvc.Settings getSettings() {
		return settings;
	}

	public void saveCharacter() {
		loader.save(equipment);
	}

	public HashMap<String, Double> getDpsBreakdown() {
		return fast.getDpsBreakdown();
	}
	
	public boolean hasChapter2() {
		int[] CHAPTER_2_ACHIEVEMENTS = {7534, 8008};
		boolean hasChapter2 = false;
		for(int i = 0; i < CHAPTER_2_ACHIEVEMENTS.length; i++){
			hasChapter2 = getLoader().getBase().getAchievements().contains(CHAPTER_2_ACHIEVEMENTS[i]);
			if(hasChapter2) return true;
		}
		return false;
	}
	
	public void reforge(){
		if(fast == null){
			System.out.println("fast == null error");
			return;
		}
		cap = new HashMap<String,Integer>();
		cap.put("style_hit", 2550);
		cap.put("spell_hit", 2550);
		cap.put("white_hit", 9010);
		cap.put("mh_exp", 2550 - getRaceWeaponSpecilizationReduction(Slot.mainHand));
		cap.put("oh_exp", 2550 - getRaceWeaponSpecilizationReduction(Slot.offHand));
		
		StatBlock stats = equipment.getStatsAsBlock();
		stats.subtract(equipment.getReforgingsAsBlock());

		list_reforges = new ArrayList<ArrayList<ReforgeOption>>();
		itemMaxEpDelta = new HashMap<Item, Double>();
		
		Stat[] rank = getSecondaryStatRanking();
		
		for(Slot slot : equipment.keySet()){
			double maxItemDelta = 0.0;
			ItemEquipped ie = equipment.get(slot);
			ArrayList<ReforgeOption> reforgeOptions = getReforgeOptions2(ie.getItem());
			if(reforgeOptions.size() <= 1)
				continue;
			ArrayList<ReforgeOption> reforgeOptionsRemove = new ArrayList<ReforgeOption>();
			for(ReforgeOption option : reforgeOptions){
				if(option.getFrom() == null) continue;
				if(option.getTo() == Stat.hit) continue;
				double fromEP = fast.getEP().get(option.getFrom().getEpName());
				double toEP = fast.getEP().get(option.getTo().getEpName());
				System.out.println(fromEP + " " + toEP);
				if(fromEP > toEP && option.getFrom() != Stat.exp){
					reforgeOptionsRemove.add(option);
				}
				
			}
			reforgeOptions.removeAll(reforgeOptionsRemove);
			System.out.println(ie.getItem().getName());
			for(ReforgeOption option : reforgeOptions){
				if(option.getFrom() != null){
					System.out.println(option.getFrom() + "->" + option.getTo());
					double EPdelta = this.calculateEPMaxDelta(option.getFrom(), option.getDelta());
					if(EPdelta > maxItemDelta){
						maxItemDelta = EPdelta;
					}
				}
			}
			itemMaxEpDelta.put(ie.getItem(), maxItemDelta);
			
			// sort reforgeOptions
			System.out.println(reforgeOptions);
			Collections.sort(reforgeOptions, new SortByStats());
			System.out.println(reforgeOptions);
			
			list_reforges.add(reforgeOptions);
		}
		
		// Calculate max EP delta for sublists
        maxEpDelta = new double[list_reforges.size()];
        for (int i = 0; i < list_reforges.size(); ++i) {
            float sublistEpDelta = 0;
            for (int j = i + 1; j < list_reforges.size(); ++j) {
                Item item = list_reforges.get(j).get(0).getItem();
                sublistEpDelta += itemMaxEpDelta.get(item);
            }
            maxEpDelta[i] = sublistEpDelta;
        }
        bestOptions = new int[list_reforges.size()];
        
        this.calcFast();
        bestEP = this.getDPS();
        long start = System.currentTimeMillis();
		bestOptions(stats, 0);
		System.out.println("called " + called);
		
		// Decode result array
        results = new ArrayList<ReforgeOption>();
        for (int i = 0; i < bestOptions.length; ++i) {
            if (bestOptions[i] != -1) {
                results.add(list_reforges.get(i).get(bestOptions[i]));
            }
        }
        
        for(ReforgeOption option : results){
        	System.out.println(option.getItem());
        	System.out.println(option.getFrom() + " -> " + option.getTo());
        }
        long end = System.currentTimeMillis();
        System.out.println("Time: " + (end-start)/1000. + " sec");
	}
	
	private boolean bestOptions(StatBlock stats, int depth){
		boolean improved = false;
		ArrayList<ReforgeOption> current_options = list_reforges.get(depth);
		

		// Base case
        if (depth + 1 == maxEpDelta.length) {
            for (int i = 0; i < current_options.size(); ++i) {
            	ReforgeOption option = current_options.get(i);
    			stats.add(option);
                //double afterReforgingEP = calcReforgeBlockEP(stats);
                this.calcFast(stats);
                double afterReforgingEP = this.getDPS();
                //System.out.println(option.getItem() + " " + option.getFrom() + " " + option.getTo());
                //System.out.println(afterReforgingEP);
                if (afterReforgingEP > bestEP) {
                    bestEP = afterReforgingEP;
                    bestOptions[depth] = i;
                    improved = true;
                }
                stats.subtract(option);
            }
            return improved;
        }
		
		//double currentMaxEpDelta = maxEpDelta[depth];
		for(int i = 0; i < current_options.size(); ++i){
			ReforgeOption option = current_options.get(i);
			stats.add(option);
			//double afterReforgeEP = calcReforgeBlockEP(stats);
			//if (afterReforgeEP + currentMaxEpDelta > bestEP) {
                if (bestOptions(stats, depth + 1)) {
                    bestOptions[depth] = i;
                    improved = true;
                }
            //}
			stats.subtract(option);
		}
		
//		double currentEP = calcReforgeBlockEP(stats);
//        if (currentEP + currentMaxEpDelta > bestEP) {
//            if (bestOptions(stats, depth + 1)) {
//                bestOptions[depth] = -1;
//                improved = true;
//            }
//        }
		
		return improved;
	}
	
	public Stat getWorstSecondaryStat(){
		Stat worst = null;
		for(Stat stat : Arrays.asList(Stat.cri,Stat.hst,Stat.mst)){
			if(worst == null || fast.getEP().get(stat.getEpName()) < fast.getEP().get(worst.getEpName())){
				worst = stat;
			}
		}
		return worst;
	}
	
	public Stat[] getSecondaryStatRanking(){
		ArrayList<Stat> stats = new ArrayList<Stat>();
		stats.add(Stat.cri);
		stats.add(Stat.hst);
		stats.add(Stat.mst);
		HashMap<Stat,Double> statEP = new HashMap<Stat,Double>();
		
		for(Stat stat : stats){
			statEP.put(stat, fast.getEP().get(stat.getEpName()));
		}
		
		Stat[] ranking = new Stat[3];
		Stat best = null;
		Stat best2 = null;
		if(statEP.get(Stat.mst) > statEP.get(Stat.hst)){
			best = Stat.mst;
			best2 = Stat.hst;
		}else{
			best = Stat.hst;
			best2 = Stat.mst;
		}
		if(statEP.get(Stat.cri) > statEP.get(best)){
			best2 = best;
			best = Stat.cri;
		} else if(statEP.get(Stat.cri) > statEP.get(best2)){
			best2 = Stat.cri;
		}
		
		ranking[0] = best;
		ranking[1] = best2;
		
		stats.remove(best);
		stats.remove(best2);
		ranking[2] = stats.get(0);
		
		return ranking;
	}
	
	public Stat getBestSecondaryStat(){
		Stat best = null;
		for(Stat stat : Arrays.asList(Stat.cri,Stat.hst,Stat.mst)){
			if(best == null || fast.getEP().get(stat.getEpName()) > fast.getEP().get(best.getEpName())){
				best = stat;
			}
		}
		return best;
	}
	
	public Stat getBestStat(){
		Stat best = null;
		for(Stat stat : Arrays.asList(Stat.exp,Stat.hit,Stat.cri,Stat.hst,Stat.mst)){
			if(best == null || fast.getEP().get(stat.getEpName()) > fast.getEP().get(best.getEpName())){
				best = stat;
			}
		}
		return best;
	}
	
	public double calculateEPMaxDelta(Stat from, double amount) {
        switch (from) {
	        case cri:
	            return (fast.getEP().get("style_hit") - fast.getEP().get("crit")) * amount;
	        case hst:
	            return (fast.getEP().get("style_hit") - fast.getEP().get("haste")) * amount;
	        case mst:
	            return (fast.getEP().get("style_hit") - fast.getEP().get("mastery")) * amount;
	        case hit:
	            return (fast.getEP().get(getBestStat().getEpName()) - fast.getEP().get("white_hit")) * amount;
	        case exp:
	            return (fast.getEP().get("style_hit") - fast.getEP().get("exp")) * amount;
	        default:
				return 0;
	        }
    }
	
	class ReforgeOption extends StatBlock {
		Item item = null;
		Reforge reforge = null;
		double delta = 0.0;
		
		public ReforgeOption(Item item, Reforge ref){
			this.item = item;
			this.reforge = ref;
			if(ref != null){
				double from_value = item.getStat(ref.getFrom());
				delta = Math.floor(from_value * 0.4);
				this.setStat(ref.getFrom(), -delta);
				this.setStat(ref.getTo(), delta);
			}
		}
		
		public Item getItem() {
			return item;
		}

		public Stat getFrom(){
			if(reforge == null)
				return null;
			return reforge.getFrom();
		}
		
		public Stat getTo(){
			if(reforge == null)
				return null;
			return reforge.getTo();
		}
		
		public double getDelta(){
			return delta;
		}
		
	}
	
	public class SortByStats implements Comparator<ReforgeOption> {
	    public int compare(ReforgeOption p1, ReforgeOption p2) {
	        return (int)-(p1.delta - p2.delta);
	    }
	}
}
